Lossy compression in graphics! Woohoo, I love this topic!! Could it be more beautiful? It probably could, but it still excites me pretty well.
But... only one image? No way, it would be too boring! In this homework I decided to bet on... 22 pictures! Why exactly 22? Well, as a big longtime fan of cartoon series Total drama and an uncle to three lovely kids aged between 8 and 10, in last weeks I have been coding a window application in Java that simulates the lives of these characters and is also a game for a few. To create it, I also prepared the icons of 22 original contestants of the first season - 300x300 jpg icons I placed in the images folder.
from os import listdir
from os.path import isfile, join
images_path = './images'
file_paths = []
contestant_names = []
n_contestants = len(listdir(images_path))
for im in listdir(images_path):
file_paths.append(images_path + '/' + im)
contestant_names.append(im[0:-4].capitalize())
import pandas as pd
pd.DataFrame({'File paths': file_paths, 'Contestant names': contestant_names})
Great! We already have lists of their file paths and names. Let's visualize them to make sure everything was successful!
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
fig, axs = plt.subplots(5, 5, figsize = (80, 80))
images = [0] * n_contestants
for i in range(len(file_paths)):
img = mpimg.imread(file_paths[i])
imgplot = axs[i//5, i%5].imshow(img)
axs[i//5, i%5].set_title(contestant_names[i], fontsize = 100)
images[i] = img
Loading all my favorite smileys from Total Drama Island went according to plan!
It shouldn't be difficult. Out of curiosity, using the RGB property, I will also see average brightness of graphics. And having them, why not show the brightest and the darkest picture?
I will create two new lists - one in 1d with just 270 000 numbers (to facilitate the coding of the extra operations mentioned above) and one in 2d - 300x3 for width & colors and 300 for height.
import numpy as np
brightest_image = images[0]
darkest_image = images[0]
minimum_rgb_avg = 255
maximum_rgb_avg = 0
brightest_con_name = contestant_names[0]
darkest_con_name = contestant_names[0]
images_reshaped = [0] * n_contestants
for i in range(len(images)):
im = images[i]
# reshaping
reshaped1d = np.reshape(np.array(im), 300 * 300 * 3) # 300 - width, 300 - height, 3 - RGB format
reshaped2d = np.reshape(np.array(im), (300 * 3, 300))
images_reshaped[i] = reshaped2d
# brightness info
avg_rgb = np.mean(reshaped1d)
print("Average pixel brightness of " + contestant_names[i] + ": " + str(avg_rgb))
# searching for the brightest and the darkest
if avg_rgb > maximum_rgb_avg:
maximum_rgb_avg = avg_rgb
brightest_image = im
brightest_con_name = contestant_names[i]
elif avg_rgb < minimum_rgb_avg:
minimum_rgb_avg = avg_rgb
darkest_image = im
darkest_con_name = contestant_names[i]
plt.imshow(brightest_image)
plt.title("Brightest contestant: " + brightest_con_name)
plt.show()
plt.imshow(darkest_image)
plt.title("Darkest contestant: " + darkest_con_name)
plt.show()
I think everything went according to plan! The black-haired Justin turned out to be the darkest, and the lightest - a thick, light-skinned Owen with a white T-shirt. Makes sense!
What about creating a 2D arrays?
print(images_reshaped[2].shape)
print(images_reshaped[n_contestants-1])
print(reshaped1d)
Looks correct also.
So now we have 22 images, every of them in 300 x 300 x 3 number list. I guess for such small numbers they are presented as 4 byte ints? That would gave 300 x 300 x 3 x 4 bytes = 1 080 000 bytes = ~1 megabyte. But who knows, maybe Python is smarter than I expected and seeing numbers in the range <0; 255> converted each to one byte? Then we would have 300 x 300 x 3 bytes = 270 000 bytes = ~264 kilobytes! Let's check it out.
import sys
for i in range(n_contestants):
print(contestant_names[i] + "' size: " + str(images_reshaped[i].nbytes))
Woah! I'm really positively surprised. In C++ we couldn't do such a thing, at least easily and automatically. Python rules!!
So now we have ~264 kilobytes for every image.
In the next part I intend to call the PCA algorithm for all images on six numbers of components: 1, 2, 4 and 16. I will write the resulting matrices to the tables and check the sizes - will I really be able to reduce the memory?
from sklearn.decomposition import PCA
import copy
analyzed_n_comp = [1, 2, 4, 16]
anc_len = len(analyzed_n_comp)
pcas = [0] * n_contestants
for i in range(n_contestants):
pcas[i] = [0] * anc_len
sizes = copy.deepcopy(pcas)
pcas_transformed = copy.deepcopy(pcas)
for i in range(n_contestants):
for j in range(anc_len):
pca = PCA(analyzed_n_comp[j])
pcas[i][j] = pca
pcas_transformed[i][j] = pca.fit_transform(images_reshaped[i])
sizes[i][j] = pcas_transformed[i][j].nbytes
print(sizes)
Comparing the resulting arrays for the original 270 000 bytes, in each case factually I got a smaller amount! Importantly - the number of bytes after calling PCA is constant- the same for each image.
We can see a simple dependency - for every 1 component we have 7200 bytes. Given that each image has 300 x 300 = 270,000 pixels, the situation is really satisfying. That means, setting PCA for less than 38 components, we always get less compression in terms of disk space!
Unfortunately, each of these compressions will be lossy - it will not be a perfect reproduction of images from the original. Initially, we got raster graphics, pixel by pixel - here from vectors... we also generate raster graphics paradoxically, but we do not get 1: 1 what was at the beginning.
Let's display the results for Beth - at least four elements for each number of components.
for j in range(anc_len):
print(analyzed_n_comp[j], "components:")
print(pcas_transformed[0][j][0:4])
Ok, time for icing on the cake. Let's see how PCA will manage for our participants - for component numbers 1, 2, 4 and 16!
for i in range(anc_len):
print("n of components:", analyzed_n_comp[i])
fig, axs = plt.subplots(5, 5, figsize=(80, 80))
for j in range(n_contestants):
approximation = pcas[j][i].inverse_transform(pcas_transformed[j][i])
app = approximation.reshape((300, 300, 3))
app /= 256
for x in range(300):
for y in range(300):
for col in range(3):
if app[x][y][col] < 0:
app[x][y][col] = 0
elif app[x][y][col] > 1:
app[x][y][col] = 1
axs[j//5, j%5].set_title(contestant_names[j], fontsize = 100)
axs[j//5, j%5].imshow(app)
plt.show()